﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using DistributedGame.Service;
using System.Threading.Tasks;
using System.ComponentModel;
using JpLabs.Extensions;
using System.Runtime.Serialization;

namespace DistributedGame.Server
{
	public enum GameState
	{
		Created,
		Opening,
		Open,
		Running, //aka Playing
		Closing,
		Closed
	}

	public class ClientSession
	{
		public ClientConnectionInfo Info { get; private set; }
		public IGameClientCallback Callback { get; private set; }
		
		public ClientSession(ClientConnectionInfo info, IGameClientCallback callback)
		{
			this.Info = info;
			this.Callback = callback;
		}
	}

	public class ClientSessionEventArgs : EventArgs
	{
		public ClientSession Client { get; private set; }

		public ClientSessionEventArgs(ClientSession client)
		{
			this.Client = client;
		}
	}

	public class RemoteMessageEventArgs : ClientSessionEventArgs
	{
		public RemoteMessage Message { get; private set; }

		public RemoteMessageEventArgs(RemoteMessage message, ClientSession client) : base(client)
		{
			this.Message = message;
		}
	}

    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
	public class GameHost : IGameHost, IDisposable//, ISupportInitialize
	{
		private object syncRoot = new object();

		private GameState State = GameState.Created;

		private Lazy<IDictionary<string,ClientSession>>
			lazyClients = new Lazy<IDictionary<string,ClientSession>>(
				() => new Dictionary<string,ClientSession>()
			);

		public event EventHandler<ClientSessionEventArgs> ClientConnected;
		public event EventHandler<ClientSessionEventArgs> ClientChannelClosed;
		public event EventHandler<ClientSessionEventArgs> ClientChannelFaulted;
		public event EventHandler<ClientSessionEventArgs> ClientDisconnected;

		public event EventHandler<RemoteMessageEventArgs> MessageReceived;

		//TODO: use a ConcurrentDictionary and get rid of locks
		//	- problem: I'm using two keys: SessionId (as a PK) and Nickname (as an UK)
		//	- solution 1: two dicts (one for each key)
		//	- solution 2: use locks for updates only (?!)

		public void SetOpening()
		{
			lock (this.syncRoot)
			{
				if (this.State != GameState.Created)
				if (this.State != GameState.Closed) throw new InvalidOperationException("Host must be in Create state in order to be Opened");
				this.State = GameState.Opening;
			}

        	this.Clients.Clear();
		}

		public void SetOpen()
		{
			lock (this.syncRoot)
			{
				if (this.State != GameState.Opening) throw new InvalidOperationException();
				this.State = GameState.Open;
			}
		}

		public void SetClosing()
		{
			lock (this.syncRoot)
			{
				this.State = GameState.Closing;
			}
		}

		public void SetClosed()
		{
			lock (this.syncRoot)
			{
				if (this.State != GameState.Closing) throw new InvalidOperationException();
				this.State = GameState.Closed;
			}

        	this.Clients.Clear();
		}

		public static string CurrentSessionId
		{
		    get {
				var context = OperationContext.Current;
				return (context == null) ? null : context.SessionId;
			}
		}

		public IDictionary<string,ClientSession> Clients
		{
			get { return lazyClients.Value; }
			//get {
			//    return (lazyConnectedClients == null)
			//        ? (lazyConnectedClients = new Dictionary<string,GameClientHandle>())
			//        : lazyConnectedClients;
			//}
		}

        public static void Broadcast(IEnumerable<ClientSession> clients, Action<ClientSession> action)
        {
			var openClients = clients.Where( c => c.Callback.AsClientChannel().State == CommunicationState.Opened );

			Parallel.ForEach(openClients, action);
        }

		public void Dispose()
		{
			this.SetClosing();

			var clients = this.GetClientList();

			if (clients.Count > 0) {
            	Broadcast( clients,  c => c.Callback.ServerClosing() ); //c.Callback.AsClientChannel().Close();
			}

			this.SetClosed();
		}

		ClientConnectionInfo IGameHost.Connect(ClientConnectRequest request)
		{
			var client = this.ConnectClient(request);

			var channel = OperationContext.Current.Channel;
			string newPlayersNickname = client.Info.Nickname;

			OperationContext.Current.OperationCompleted += (//CustomEvent.ToSynced(this,
				(sender, e) => {
					this.ClientConnected.RaiseEvent(this, new ClientSessionEventArgs(client));

					var joinedMsg = new RemoteMessage(MessageFormatter.ClientJoinedMessage(newPlayersNickname));
					var welcomeMsg = new RemoteMessage(MessageFormatter.WelcomeMessage(newPlayersNickname));

					Parallel.Invoke(
					    () => client.Callback.Receive(welcomeMsg),
					    () => this.BroadcastMessage(joinedMsg, true)
					);
				}
			);

			OperationContext.Current.Channel.Closed += (//CustomEvent.ToSynced(this,
				(sender, e) => {
					this.ClientChannelClosed.RaiseEvent(this, new ClientSessionEventArgs(client));
				}
			);

			//channel.Extensions.Add(IExtension<IContextChannel>

			OperationContext.Current.Channel.Faulted += (//CustomEvent.ToSynced(this,
				(sender, e) => {
					this.ClientChannelFaulted.RaiseEvent(this, new ClientSessionEventArgs(client));

					////channel.Extensions

					////var senderAsChannel = sender as IContextChannel;
					////var exception1 = channel.GetProperty<Exception>();
					////var exception2 = senderAsChannel.GetProperty<Exception>();

					//TextEntered(newPlayersNickname + " faulted?!");
					////this.Dispatcher.BeginInvoke((Action<string>)TextEntered, newPlayersNick + " faulted?!");

					////System.ServiceModel.Dispatcher.ChannelDispatcher

					////http://stackoverflow.com/questions/381345/in-wcf-in-the-faulted-event-how-do-i-get-the-exception-details

					////Extending WCF: http://msdn.microsoft.com/en-us/library/ms733848.aspx

					////http://www.codeproject.com/KB/WCF/WCFErrorHandling.aspx
				}
			);
						
			return client.Info;
		}

		void IGameHost.Say(string textMessage)
		{
			var client = this.GetCurrentClient();
			if (client == null) throw new InvalidOperationException("Client is not authenticated");
			
			//TODO: Parse messages
			//www.ircbeginner.com/ircinfo/m-commands.htm
			//www.ircbeginner.com/ircinfo/ircc-commands.html

			//TODO: broadcast message at OperationContext.Current.OperationCompleted

			var message = new ClientMessage(client.Info, textMessage);

			OperationContext.Current.OperationCompleted += (//CustomEvent.ToSynced(this,
				(sender, e) => {
					this.BroadcastMessage(message, false);

					this.MessageReceived.RaiseEvent(this, new RemoteMessageEventArgs(message, client));
				}
			);
			
			//messageText = MessageFormatter.ClientMessage(messageText, client.Info.Nickname);
			////this.Dispatcher.InvokeIfNeeded( (Action<string>)AddChatText, messageText );
			//this.BroadcastMessage(messageText);
		}

		void IGameHost.Disconnect()
		{
			var client = this.GetCurrentClient();
			if (client == null) throw new InvalidOperationException("Client is not authenticated");

			this.DisconnectClient(client);

			OperationContext.Current.OperationCompleted += (//CustomEvent.ToSynced(this,
				(sender, e) => {
					this.ClientDisconnected.RaiseEvent(this, new ClientSessionEventArgs(client));

					//UpdatePlayerList();

					//string text = MessageFormatter.ClientIsDisconnectingMessage(client.Info.Nickname);
					//AddChatText(text);
					//this.GameServer.BroadcastMessage(text);
				}
			);
		}


		public ICollection<ClientSession> GetClientList()
		{
			lock (syncRoot) return this.Clients.Values.ToReadOnlyColl();
		}

        public ClientSession GetCurrentClient()
        {
			ClientSession client;
			return this.Clients.TryGetValue(CurrentSessionId, out client) ? client : null;
        }

		private ClientSession ConnectClient(ClientConnectRequest request)
		{
			if (request == null) throw new ArgumentNullException("request");
			if (string.IsNullOrEmpty(request.DesiredNickname)) throw new ArgumentException("Invalid Name");

			if (this.State != GameState.Open) throw new InvalidOperationException("Can't connect clients in this state");
			
			if (this.GetCurrentClient() != null) throw new InvalidOperationException("Client already connected");
			
			string nickname = request.DesiredNickname;
			
			lock (syncRoot)
			{
				//Rename player name to avoid name conflicts
				var currentNames = this.Clients.Values.Select( c => c.Info.Nickname ).ToArray();

				int attempt = 0;
				while (currentNames.Contains(nickname)) nickname = string.Format("{0}[{1}]", request.DesiredNickname, ++attempt);
				
				string sessionId = CurrentSessionId;
				var info = new ClientConnectionInfo() { SessionId = sessionId, Nickname = nickname };
				var client = new ClientSession(info, GameServer.GetCurrentClientCallback());
				
				this.Clients.Add(sessionId, client);

				return client;
			}
		}

		private void DisconnectClient(ClientSession client)
		{
			if (client == null) throw new ArgumentNullException("client");

			lock (syncRoot)
			{
				this.Clients.Remove(client.Info.SessionId);
			}
		}

        public void BroadcastMessage(RemoteMessage message, bool loopBack)
        {
			//TODO: verify Host state (or not)

			Broadcast(
				this.GetClientList(),
				client => { client.Callback.Receive(message); }
			);

			if (loopBack) this.MessageReceived.RaiseEvent(this, new RemoteMessageEventArgs(message, null));
        }
	}
}
